/*******************************************************************************
#                                                                              #
#      MJPG-streamer allows to stream JPG frames from an input-plugin          #
#      to several output plugins                                               #
#                                                                              #
#      Copyright (C) 2007 Tom Stöveken                                         #
#                                                                              #
# This program is free software; you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by         #
# the Free Software Foundation; version 2 of the License.                      #
#                                                                              #
# This program is distributed in the hope that it will be useful,              #
# but WITHOUT ANY WARRANTY; without even the implied warranty of               #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                #
# GNU General Public License for more details.                                 #
#                                                                              #
# You should have received a copy of the GNU General Public License            #
# along with this program; if not, write to the Free Software                  #
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA    #
#                                                                              #
*******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <getopt.h>
#include <pthread.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "../../mjpg_streamer.h"
#include "../../utils.h"

#define INPUT_PLUGIN_NAME "FILE input plugin"
#define MAX_ARGUMENTS 32

/* private functions and variables to this plugin */
static pthread_t   worker;
static globals     *pglobal;

void *worker_thread( void *);
void worker_cleanup(void *);
void help(void);

static int delay = 0;
static char *folder = NULL;
static char *filename = NULL;
static int rm = 0;

/* global variables for this plugin */
static int fd, rc, wd, size;
static struct inotify_event *ev;

/*** plugin interface functions ***/
int input_init(input_parameter *param) {
  char *argv[MAX_ARGUMENTS]={NULL};
  int argc=1, i;

  /* convert the single parameter-string to an array of strings */
  argv[0] = INPUT_PLUGIN_NAME;
  if ( param->parameter_string != NULL && strlen(param->parameter_string) != 0 ) {
    char *arg=NULL, *saveptr=NULL, *token=NULL;

    arg=(char *)strdup(param->parameter_string);

    if ( strchr(arg, ' ') != NULL ) {
      token=strtok_r(arg, " ", &saveptr);
      if ( token != NULL ) {
        argv[argc] = strdup(token);
        argc++;
        while ( (token=strtok_r(NULL, " ", &saveptr)) != NULL ) {
          argv[argc] = strdup(token);
          argc++;
          if (argc >= MAX_ARGUMENTS) {
            IPRINT("ERROR: too many arguments to input plugin\n");
            return 1;
          }
        }
      }
    }
  }

  /* show all parameters for DBG purposes */
  for (i=0; i<argc; i++) {
    DBG("argv[%d]=%s\n", i, argv[i]);
  }

  reset_getopt();
  while(1) {
    int option_index = 0, c=0;
    static struct option long_options[] = \
    {
      {"h", no_argument, 0, 0},
      {"help", no_argument, 0, 0},
      {"d", required_argument, 0, 0},
      {"delay", required_argument, 0, 0},
      {"f", required_argument, 0, 0},
      {"folder", required_argument, 0, 0},
      {"r", no_argument, 0, 0},
      {"remove", no_argument, 0, 0},
      {"n", required_argument, 0, 0},
      {"name", required_argument, 0, 0},
      {0, 0, 0, 0}
    };

    c = getopt_long_only(argc, argv, "", long_options, &option_index);

    /* no more options to parse */
    if (c == -1) break;

    /* unrecognized option */
    if (c == '?'){
      help();
      return 1;
    }

    switch (option_index) {
      /* h, help */
      case 0:
      case 1:
        DBG("case 0,1\n");
        help();
        return 1;
        break;

      /* d, delay */
      case 2:
      case 3:
        DBG("case 2,3\n");
        delay = atoi(optarg);
        break;

      /* f, folder */
      case 4:
      case 5:
        DBG("case 4,5\n");
        folder = malloc(strlen(optarg)+2);
        strcpy(folder, optarg);
        if ( optarg[strlen(optarg)-1] != '/' )
          strcat(folder, "/");
        break;

      /* r, remove */
      case 6:
      case 7:
        DBG("case 6,7\n");
        rm = 1;
        break;

      /* n, name */
      case 8:
      case 9:
        DBG("case 8,9\n");
        filename = malloc(strlen(optarg)+2);
        strcpy(filename, optarg);
        break;

      default:
        DBG("default case\n");
        help();
        return 1;
    }
  }

  pglobal = param->global;

  /* check for required parameters */
  if ( folder == NULL ) {
    IPRINT("ERROR: no folder specified\n");
    return 1;
  }

  IPRINT("folder to watch...: %s\n", folder);
  IPRINT("forced delay......: %i\n", delay);
  IPRINT("delete file.......: %s\n", (rm)?"yes, delete":"no, do not delete");
  IPRINT("filename must be..: %s\n", (filename==NULL)?"-no filter for certain filename set-":filename);

  return 0;
}

int input_stop(void) {
  DBG("will cancel input thread\n");
  pthread_cancel(worker);

  return 0;
}

int input_run(void) {
  pglobal->buf = NULL;

  rc = fd = inotify_init();
  if ( rc == -1 ) {
    perror("could not initilialize inotify");
    return 1;
  }

  rc = wd = inotify_add_watch(fd, folder, IN_CLOSE_WRITE|IN_MOVED_TO|IN_ONLYDIR);
  if ( rc == -1 ) {
    perror("could not add watch");
    return 1;
  }

  size = sizeof(struct inotify_event) + (1<<16);
  ev = malloc(size);
  if ( ev == NULL ) {
    perror("not enough memory");
    return 1;
  }

  if( pthread_create(&worker, 0, worker_thread, NULL) != 0) {
    free(pglobal->buf);
    fprintf(stderr, "could not start worker thread\n");
    exit(EXIT_FAILURE);
  }

  pthread_detach(worker);

  return 0;
}

/*** private functions for this plugin below ***/
void help(void) {
    fprintf(stderr, " ---------------------------------------------------------------\n" \
                    " Help for input plugin..: "INPUT_PLUGIN_NAME"\n" \
                    " ---------------------------------------------------------------\n" \
                    " The following parameters can be passed to this plugin:\n\n" \
                    " [-d | --delay ]........: delay to pause between frames\n" \
                    " [-f | --folder ].......: folder to watch for new JPEG files\n" \
                    " [-r | --remove ].......: remove/delete JPEG file after reading\n" \
                    " [-n | --name ].........: ignore changes unless filename matches\n" \
                    " ---------------------------------------------------------------\n");
}

/* the single writer thread */
void *worker_thread( void *arg ) {
  char buffer[1<<16];
  int file;
  size_t filesize = 0;
  struct stat stats;

  /* set cleanup handler to cleanup allocated ressources */
  pthread_cleanup_push(worker_cleanup, NULL);

  while( !pglobal->stop ) {

    /* wait for new frame, read will block until something happens */
    rc = read(fd, ev, size);
    if ( rc == -1 ) {
      perror("reading inotify events failed");
      break;
    }

    /* sanity check */
    if ( wd != ev->wd ) {
      fprintf(stderr, "This event is not for the watched directory (%d != %d)\n", wd, ev->wd);
      continue;
    }

    if ( ev->mask & (IN_IGNORED|IN_Q_OVERFLOW|IN_UNMOUNT) ) {
      fprintf(stderr, "event mask suggests to stop\n");
      break;
    }

    /* prepare filename */
    snprintf(buffer, sizeof(buffer), "%s%s", folder, ev->name);

    /* check if the filename matches specified parameter (if given) */
    if ( (filename != NULL) && (strcmp(filename, ev->name) != 0) ) {
      DBG("ignoring this change (specified filename does not match)\n");
      continue;
    }
    DBG("new file detected: %s\n", buffer);

    /* open file for reading */
    rc = file = open(buffer, O_RDONLY);
    if ( rc == -1 ) {
      perror("could not open file for reading");
      break;
    }

    /* approximate size of file */
    rc = fstat(file, &stats);
    if ( rc == -1 ) {
      perror("could not read statistics of file");
      close(file);
      break;
    }

    filesize = stats.st_size;

    /* copy frame from file to global buffer */
    pthread_mutex_lock( &pglobal->db );

    /* allocate memory for frame */
    if ( pglobal->buf != NULL ) free(pglobal->buf);
    pglobal->buf = malloc(filesize + (1<<16));
    if (pglobal->buf == NULL) {
      fprintf(stderr, "could not allocate memory\n");
      break;
    }

    if ( (pglobal->size = read(file, pglobal->buf, filesize)) == -1) {
      perror("could not read from file");
      free(pglobal->buf); pglobal->buf = NULL; pglobal->size = 0;
      pthread_mutex_unlock( &pglobal->db );
      close(file);
      break;
    }

    DBG("new frame copied (size: %d)\n", pglobal->size);

    /* signal fresh_frame */
    pthread_cond_broadcast(&pglobal->db_update);
    pthread_mutex_unlock( &pglobal->db );

    close(file);

    /* delete file if necessary */
    if (rm) {
      rc = unlink(buffer);
      if ( rc == -1 ) {
        perror("could not remove/delete file");
      }
    }

    if (delay != 0) usleep(1000*delay);
  }

  DBG("leaving input thread, calling cleanup function now\n");
  /* call cleanup handler, signal with the parameter */
  pthread_cleanup_pop(1);

  return NULL;
}

void worker_cleanup(void *arg) {
  static unsigned char first_run=1;

  if ( !first_run ) {
    DBG("already cleaned up ressources\n");
    return;
  }

  first_run = 0;
  DBG("cleaning up ressources allocated by input thread\n");

  if (pglobal->buf != NULL) free(pglobal->buf);

  free(ev);

  rc = inotify_rm_watch(fd, wd);
  if ( rc == -1 ) {
    perror("could not close watch descriptor");
  }

  rc = close(fd);
  if ( rc == -1 ) {
    perror("could not close filedescriptor");
  }
}




